{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "whjsJasuhstV"
   },
   "source": [
    "<a href=\"https://colab.research.google.com/github/jeffheaton/app_generative_ai/blob/main/t81_559_class_02_2_multi_prompt.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "euOZxlIMhstX"
   },
   "source": [
    "# T81-559: Applications of Generative Artificial Intelligence\n",
    "**Module 2: Code Generation**\n",
    "* Instructor: [Jeff Heaton](https://sites.wustl.edu/jeffheaton/), McKelvey School of Engineering, [Washington University in St. Louis](https://engineering.wustl.edu/Programs/Pages/default.aspx)\n",
    "* For more information visit the [class website](https://sites.wustl.edu/jeffheaton/t81-558/)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "d4Yov72PhstY",
    "jp-MarkdownHeadingCollapsed": true
   },
   "source": [
    "# Module 2 Material\n",
    "\n",
    "* Part 2.1: Prompting for Code Generation [[Video]](https://www.youtube.com/watch?v=HVId6kYKKgQ) [[Notebook]](t81_559_class_02_1_dev.ipynb)\n",
    "* **Part 2.2: Handling Revision Prompts** [[Video]](https://www.youtube.com/watch?v=APpV46tplXA) [[Notebook]](t81_559_class_02_2_multi_prompt.ipynb)\n",
    "* Part 2.3: Using a LLM to Help Debug [[Video]](https://www.youtube.com/watch?v=VPqSNb38QK0) [[Notebook]](t81_559_class_02_3_llm_debug.ipynb)\n",
    "* Part 2.4: Tracking Prompts in Software Development [[Video]](https://www.youtube.com/watch?v=oUFUuYfvXZU) [[Notebook]](t81_559_class_02_4_software_eng.ipynb)\n",
    "* Part 2.5: Limits of LLM Code Generation [[Video]](https://www.youtube.com/watch?v=dKtRI0LZSyY) [[Notebook]](t81_559_class_02_5_code_gen_limits.ipynb)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "AcAUP0c3hstY"
   },
   "source": [
    "# Google CoLab Instructions\n",
    "\n",
    "The following code ensures that Google CoLab is running and maps Google Drive if needed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "xsI496h5hstZ",
    "outputId": "0106e4b9-a88e-4422-8ce6-258b78fd230e"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Note: using Google CoLab\n",
      "Collecting langchain\n",
      "  Downloading langchain-0.1.16-py3-none-any.whl (817 kB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m817.7/817.7 kB\u001b[0m \u001b[31m4.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hCollecting langchain_openai\n",
      "  Downloading langchain_openai-0.1.4-py3-none-any.whl (33 kB)\n",
      "Requirement already satisfied: PyYAML>=5.3 in /usr/local/lib/python3.10/dist-packages (from langchain) (6.0.1)\n",
      "Requirement already satisfied: SQLAlchemy<3,>=1.4 in /usr/local/lib/python3.10/dist-packages (from langchain) (2.0.29)\n",
      "Requirement already satisfied: aiohttp<4.0.0,>=3.8.3 in /usr/local/lib/python3.10/dist-packages (from langchain) (3.9.5)\n",
      "Requirement already satisfied: async-timeout<5.0.0,>=4.0.0 in /usr/local/lib/python3.10/dist-packages (from langchain) (4.0.3)\n",
      "Collecting dataclasses-json<0.7,>=0.5.7 (from langchain)\n",
      "  Downloading dataclasses_json-0.6.4-py3-none-any.whl (28 kB)\n",
      "Collecting jsonpatch<2.0,>=1.33 (from langchain)\n",
      "  Downloading jsonpatch-1.33-py2.py3-none-any.whl (12 kB)\n",
      "Collecting langchain-community<0.1,>=0.0.32 (from langchain)\n",
      "  Downloading langchain_community-0.0.34-py3-none-any.whl (1.9 MB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.9/1.9 MB\u001b[0m \u001b[31m23.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hCollecting langchain-core<0.2.0,>=0.1.42 (from langchain)\n",
      "  Downloading langchain_core-0.1.46-py3-none-any.whl (299 kB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m299.3/299.3 kB\u001b[0m \u001b[31m11.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hCollecting langchain-text-splitters<0.1,>=0.0.1 (from langchain)\n",
      "  Downloading langchain_text_splitters-0.0.1-py3-none-any.whl (21 kB)\n",
      "Collecting langsmith<0.2.0,>=0.1.17 (from langchain)\n",
      "  Downloading langsmith-0.1.51-py3-none-any.whl (115 kB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m116.0/116.0 kB\u001b[0m \u001b[31m4.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hRequirement already satisfied: numpy<2,>=1 in /usr/local/lib/python3.10/dist-packages (from langchain) (1.25.2)\n",
      "Requirement already satisfied: pydantic<3,>=1 in /usr/local/lib/python3.10/dist-packages (from langchain) (2.7.0)\n",
      "Requirement already satisfied: requests<3,>=2 in /usr/local/lib/python3.10/dist-packages (from langchain) (2.31.0)\n",
      "Requirement already satisfied: tenacity<9.0.0,>=8.1.0 in /usr/local/lib/python3.10/dist-packages (from langchain) (8.2.3)\n",
      "Collecting openai<2.0.0,>=1.10.0 (from langchain_openai)\n",
      "  Downloading openai-1.23.6-py3-none-any.whl (311 kB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m311.6/311.6 kB\u001b[0m \u001b[31m11.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hCollecting tiktoken<1,>=0.5.2 (from langchain_openai)\n",
      "  Downloading tiktoken-0.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.8 MB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.8/1.8 MB\u001b[0m \u001b[31m21.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hRequirement already satisfied: aiosignal>=1.1.2 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.3.1)\n",
      "Requirement already satisfied: attrs>=17.3.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (23.2.0)\n",
      "Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.4.1)\n",
      "Requirement already satisfied: multidict<7.0,>=4.5 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (6.0.5)\n",
      "Requirement already satisfied: yarl<2.0,>=1.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.9.4)\n",
      "Collecting marshmallow<4.0.0,>=3.18.0 (from dataclasses-json<0.7,>=0.5.7->langchain)\n",
      "  Downloading marshmallow-3.21.1-py3-none-any.whl (49 kB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m49.4/49.4 kB\u001b[0m \u001b[31m1.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hCollecting typing-inspect<1,>=0.4.0 (from dataclasses-json<0.7,>=0.5.7->langchain)\n",
      "  Downloading typing_inspect-0.9.0-py3-none-any.whl (8.8 kB)\n",
      "Collecting jsonpointer>=1.9 (from jsonpatch<2.0,>=1.33->langchain)\n",
      "  Downloading jsonpointer-2.4-py2.py3-none-any.whl (7.8 kB)\n",
      "Collecting packaging<24.0,>=23.2 (from langchain-core<0.2.0,>=0.1.42->langchain)\n",
      "  Downloading packaging-23.2-py3-none-any.whl (53 kB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m53.0/53.0 kB\u001b[0m \u001b[31m2.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hCollecting orjson<4.0.0,>=3.9.14 (from langsmith<0.2.0,>=0.1.17->langchain)\n",
      "  Downloading orjson-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (141 kB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m141.1/141.1 kB\u001b[0m \u001b[31m812.0 kB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hRequirement already satisfied: anyio<5,>=3.5.0 in /usr/local/lib/python3.10/dist-packages (from openai<2.0.0,>=1.10.0->langchain_openai) (3.7.1)\n",
      "Requirement already satisfied: distro<2,>=1.7.0 in /usr/lib/python3/dist-packages (from openai<2.0.0,>=1.10.0->langchain_openai) (1.7.0)\n",
      "Collecting httpx<1,>=0.23.0 (from openai<2.0.0,>=1.10.0->langchain_openai)\n",
      "  Downloading httpx-0.27.0-py3-none-any.whl (75 kB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m75.6/75.6 kB\u001b[0m \u001b[31m5.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hRequirement already satisfied: sniffio in /usr/local/lib/python3.10/dist-packages (from openai<2.0.0,>=1.10.0->langchain_openai) (1.3.1)\n",
      "Requirement already satisfied: tqdm>4 in /usr/local/lib/python3.10/dist-packages (from openai<2.0.0,>=1.10.0->langchain_openai) (4.66.2)\n",
      "Requirement already satisfied: typing-extensions<5,>=4.7 in /usr/local/lib/python3.10/dist-packages (from openai<2.0.0,>=1.10.0->langchain_openai) (4.11.0)\n",
      "Requirement already satisfied: annotated-types>=0.4.0 in /usr/local/lib/python3.10/dist-packages (from pydantic<3,>=1->langchain) (0.6.0)\n",
      "Requirement already satisfied: pydantic-core==2.18.1 in /usr/local/lib/python3.10/dist-packages (from pydantic<3,>=1->langchain) (2.18.1)\n",
      "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2->langchain) (3.3.2)\n",
      "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2->langchain) (3.7)\n",
      "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2->langchain) (2.0.7)\n",
      "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2->langchain) (2024.2.2)\n",
      "Requirement already satisfied: greenlet!=0.4.17 in /usr/local/lib/python3.10/dist-packages (from SQLAlchemy<3,>=1.4->langchain) (3.0.3)\n",
      "Requirement already satisfied: regex>=2022.1.18 in /usr/local/lib/python3.10/dist-packages (from tiktoken<1,>=0.5.2->langchain_openai) (2023.12.25)\n",
      "Requirement already satisfied: exceptiongroup in /usr/local/lib/python3.10/dist-packages (from anyio<5,>=3.5.0->openai<2.0.0,>=1.10.0->langchain_openai) (1.2.1)\n",
      "Collecting httpcore==1.* (from httpx<1,>=0.23.0->openai<2.0.0,>=1.10.0->langchain_openai)\n",
      "  Downloading httpcore-1.0.5-py3-none-any.whl (77 kB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m77.9/77.9 kB\u001b[0m \u001b[31m7.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hCollecting h11<0.15,>=0.13 (from httpcore==1.*->httpx<1,>=0.23.0->openai<2.0.0,>=1.10.0->langchain_openai)\n",
      "  Downloading h11-0.14.0-py3-none-any.whl (58 kB)\n",
      "\u001b[2K     \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m58.3/58.3 kB\u001b[0m \u001b[31m6.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
      "\u001b[?25hCollecting mypy-extensions>=0.3.0 (from typing-inspect<1,>=0.4.0->dataclasses-json<0.7,>=0.5.7->langchain)\n",
      "  Downloading mypy_extensions-1.0.0-py3-none-any.whl (4.7 kB)\n",
      "Installing collected packages: packaging, orjson, mypy-extensions, jsonpointer, h11, typing-inspect, tiktoken, marshmallow, jsonpatch, httpcore, langsmith, httpx, dataclasses-json, openai, langchain-core, langchain-text-splitters, langchain_openai, langchain-community, langchain\n",
      "  Attempting uninstall: packaging\n",
      "    Found existing installation: packaging 24.0\n",
      "    Uninstalling packaging-24.0:\n",
      "      Successfully uninstalled packaging-24.0\n",
      "Successfully installed dataclasses-json-0.6.4 h11-0.14.0 httpcore-1.0.5 httpx-0.27.0 jsonpatch-1.33 jsonpointer-2.4 langchain-0.1.16 langchain-community-0.0.34 langchain-core-0.1.46 langchain-text-splitters-0.0.1 langchain_openai-0.1.4 langsmith-0.1.51 marshmallow-3.21.1 mypy-extensions-1.0.0 openai-1.23.6 orjson-3.10.1 packaging-23.2 tiktoken-0.6.0 typing-inspect-0.9.0\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "\n",
    "try:\n",
    "    from google.colab import drive, userdata\n",
    "    COLAB = True\n",
    "    print(\"Note: using Google CoLab\")\n",
    "except:\n",
    "    print(\"Note: not using Google CoLab\")\n",
    "    COLAB = False\n",
    "\n",
    "# OpenAI Secrets\n",
    "if COLAB:\n",
    "    os.environ[\"OPENAI_API_KEY\"] = userdata.get('OPENAI_API_KEY')\n",
    "\n",
    "# Install needed libraries in CoLab\n",
    "if COLAB:\n",
    "    !pip install langchain langchain_openai"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "pC9A-LaYhsta"
   },
   "source": [
    "# 2.2: Handling Revision Prompts\n",
    "\n",
    "Previously, we just sent one prompt to the LLM, which generated code. It is possible to perform this code more conversationally. In this module, we will see how to converse with the LLM to request changes to outputted code and even help the LLM to produce a more accurate model.\n",
    "\n",
    "We will also see that it might be beneficial to recreate your conversation as one single prompt that generates the final result. Keeping track of one prompt, rather than a conversation, that created your final code is more maintainable.\n",
    "\n",
    "## Conversational Code Generation\n",
    "\n",
    "We will introduce a more advanced code generation function that allows you to start the conversation to generate code and follow up with additional prompts if needed.\n",
    "\n",
    "In future modules, we will see how to create chatbots similar to this one. We will use the code I provided to generate your code for now. This generator uses a system prompt that requests that the generated code conform to the following:\n",
    "\n",
    "* Imports should be sorted\n",
    "* Code should conform to PEP-8 formatting\n",
    "* Do not mix uncompilable notes with code\n",
    "* Add comments"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "id": "TMF-rtxgRAea"
   },
   "outputs": [],
   "source": [
    "from langchain.chains import ConversationChain\n",
    "from langchain.memory import ConversationBufferWindowMemory\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.prompts.chat import PromptTemplate\n",
    "from IPython.display import display_markdown\n",
    "\n",
    "MODEL = 'gpt-4o-mini'\n",
    "TEMPLATE = \"\"\"The following is a friendly conversation between a human and an\n",
    "AI to generate Python code. If you have notes about the code, place them before\n",
    "the code. Any nots about execution should follow the code. If you do mix any\n",
    "notes with the code, make them comments. Add proper comments to the code.\n",
    "Sort imports and follow PEP-8 formatting.\n",
    "\n",
    "Current conversation:\n",
    "{history}\n",
    "Human: {input}\n",
    "Code Assistant:\"\"\"\n",
    "PROMPT_TEMPLATE = PromptTemplate(input_variables=[\"history\", \"input\"], template=TEMPLATE)\n",
    "\n",
    "def start_conversation():\n",
    "    # Initialize the OpenAI LLM with your API key\n",
    "    llm = ChatOpenAI(\n",
    "        model=MODEL,\n",
    "        temperature=0.0,\n",
    "        n=1\n",
    "    )\n",
    "\n",
    "    # Initialize memory and conversation\n",
    "    memory = ConversationBufferWindowMemory()\n",
    "    conversation = ConversationChain(\n",
    "        prompt=PROMPT_TEMPLATE,\n",
    "        llm=llm,\n",
    "        memory=memory,\n",
    "        verbose=False\n",
    "    )\n",
    "\n",
    "    return conversation\n",
    "\n",
    "def generate_code(conversation, prompt):\n",
    "    print(\"Model response:\")\n",
    "    output = conversation.invoke(prompt)\n",
    "    display_markdown(output['response'], raw=True)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "ClPhLkGldhPt"
   },
   "source": [
    "## First Attempt at an XOR Approximator\n",
    "\n",
    "We will construct a prompt that requests the LLM to generate a PyTorch neural network to approximate the [Exclusive Or](https://en.wikipedia.org/wiki/Exclusive_or). The truth table for the Exclusive Or (XOR) function is provided here:\n",
    "\n",
    "```\n",
    "0 XOR 0 = 0\n",
    "1 XOR 0 = 1\n",
    "0 XOR 1 = 1\n",
    "1 XOR 1 = 0\n",
    "```\n",
    "\n",
    "If given data, neural networks can learn to approximate functions, so let's create a PyTorch neural network to approximate the XOR function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 1000
    },
    "id": "ydaqwgRiH4D6",
    "outputId": "9ef9cde0-190c-4c38-c2a2-f80e65bef76b"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model response:\n"
     ]
    },
    {
     "data": {
      "text/markdown": [
       "Certainly! To learn the XOR function using PyTorch, we'll need to set up a simple neural network. The XOR function is a classic problem in neural network training because it's not linearly separable, which means we'll need at least one hidden layer in our network.\n",
       "\n",
       "Here's how you can set up a neural network in PyTorch to learn the XOR function:\n",
       "\n",
       "```python\n",
       "# Import necessary libraries\n",
       "import torch\n",
       "import torch.nn as nn\n",
       "import torch.optim as optim\n",
       "\n",
       "# Define the XOR class using PyTorch\n",
       "class XORNet(nn.Module):\n",
       "    def __init__(self):\n",
       "        super(XORNet, self).__init__()\n",
       "        # Neural network layers: 2 input neurons, 2 hidden neurons, 1 output neuron\n",
       "        self.layer1 = nn.Linear(2, 2)\n",
       "        self.layer2 = nn.Linear(2, 1)\n",
       "        self.sigmoid = nn.Sigmoid()\n",
       "\n",
       "    def forward(self, x):\n",
       "        x = self.sigmoid(self.layer1(x))\n",
       "        x = self.sigmoid(self.layer2(x))\n",
       "        return x\n",
       "\n",
       "# Initialize the model, loss function, and optimizer\n",
       "model = XORNet()\n",
       "criterion = nn.MSELoss()\n",
       "optimizer = optim.SGD(model.parameters(), lr=0.1)\n",
       "\n",
       "# Training data for XOR\n",
       "data = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])\n",
       "labels = torch.tensor([[0.0], [1.0], [1.0], [0.0]])\n",
       "\n",
       "# Train the model\n",
       "for epoch in range(10000):\n",
       "    # Forward pass: Compute predicted y by passing x to the model\n",
       "    pred = model(data)\n",
       "\n",
       "    # Compute and print loss\n",
       "    loss = criterion(pred, labels)\n",
       "    if epoch % 1000 == 0:\n",
       "        print(f'Epoch {epoch} Loss: {loss.item()}')\n",
       "\n",
       "    # Zero gradients, perform a backward pass, and update the weights.\n",
       "    optimizer.zero_grad()\n",
       "    loss.backward()\n",
       "    optimizer.step()\n",
       "\n",
       "# Test the model\n",
       "with torch.no_grad():\n",
       "    test_pred = model(data)\n",
       "    print(\"Predicted values:\")\n",
       "    print(test_pred)\n",
       "```\n",
       "\n",
       "### Notes on Execution:\n",
       "- Ensure you have PyTorch installed in your Python environment. You can install it using `pip install torch`.\n",
       "- The learning rate and the number of epochs are set to simple values for demonstration. You might need to adjust these depending on the convergence behavior.\n",
       "- The model uses a sigmoid activation function, which is typical for binary classification tasks like XOR.\n",
       "- The output after training should approximate the XOR truth table, with values close to 0 or 1."
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "conversation = start_conversation()\n",
    "generate_code(conversation, \"\"\"Write Python code to learn the XOR function with PyTorch.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "0hPqM7BslmM7"
   },
   "source": [
    "# Requesting a Change to Generated Code\n",
    "\n",
    "If you've taken my other course, you will know I prefer PyTorch sequences over extending the nn.Module class, at least for simple neural networks like an XOR approximator. LLMs do not share this opinion. However, the LLM will gladly humor me and generate a sequence. Here, I provide an additional prompt to request this rather than resubmitting a modified version of my first prompt."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 948
    },
    "id": "lqnDZhc4OVU6",
    "outputId": "8ef3e8c4-68c7-4001-8708-efaceead6102"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model response:\n"
     ]
    },
    {
     "data": {
      "text/markdown": [
       "Certainly! Using `torch.nn.Sequential` is a more concise way to define a model when you don't need the customizability that comes with defining your own `nn.Module` class. Here's how you can define the same XOR network using `Sequential`:\n",
       "\n",
       "```python\n",
       "# Import necessary libraries\n",
       "import torch\n",
       "import torch.nn as nn\n",
       "import torch.optim as optim\n",
       "\n",
       "# Define the XOR network using a sequential container\n",
       "model = nn.Sequential(\n",
       "    nn.Linear(2, 2),\n",
       "    nn.Sigmoid(),\n",
       "    nn.Linear(2, 1),\n",
       "    nn.Sigmoid()\n",
       ")\n",
       "\n",
       "# Initialize the loss function and optimizer\n",
       "criterion = nn.MSELoss()\n",
       "optimizer = optim.SGD(model.parameters(), lr=0.1)\n",
       "\n",
       "# Training data for XOR\n",
       "data = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])\n",
       "labels = torch.tensor([[0.0], [1.0], [1.0], [0.0]])\n",
       "\n",
       "# Train the model\n",
       "for epoch in range(10000):\n",
       "    # Forward pass: Compute predicted y by passing x to the model\n",
       "    pred = model(data)\n",
       "\n",
       "    # Compute and print loss\n",
       "    loss = criterion(pred, labels)\n",
       "    if epoch % 1000 == 0:\n",
       "        print(f'Epoch {epoch} Loss: {loss.item()}')\n",
       "\n",
       "    # Zero gradients, perform a backward pass, and update the weights.\n",
       "    optimizer.zero_grad()\n",
       "    loss.backward()\n",
       "    optimizer.step()\n",
       "\n",
       "# Test the model\n",
       "with torch.no_grad():\n",
       "    test_pred = model(data)\n",
       "    print(\"Predicted values:\")\n",
       "    print(test_pred)\n",
       "```\n",
       "\n",
       "### Notes on Execution:\n",
       "- This version of the code uses `torch.nn.Sequential` to define the model in a more compact way. The functionality remains the same as the previous example.\n",
       "- Ensure PyTorch is installed in your environment as mentioned previously.\n",
       "- The learning rate and number of epochs are kept the same for consistency with the previous example. Adjust these parameters as needed based on the model's performance.\n",
       "- The output should still approximate the XOR truth table, demonstrating the model's ability to learn the XOR function using a sequential model structure."
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "generate_code(conversation, \"\"\"Could you make use of a PyTorch sequence rather than a nn.Module class?\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "C7U1Bx3Wbje1"
   },
   "source": [
    "# Testing the Generated Code\n",
    "\n",
    "LLMs are not overachievers; they will implement the code you ask for and not provide much more. When we run the XOR approximator's first version, the results are only sometimes accurate, especially if we run the program multiple times."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "CdJNKLVhRcD3",
    "outputId": "7e690772-13d7-4ec7-813a-64b426bb8855"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 0 Loss: 0.2688503861427307\n",
      "Epoch 1000 Loss: 0.24990490078926086\n",
      "Epoch 2000 Loss: 0.24987591803073883\n",
      "Epoch 3000 Loss: 0.2498343586921692\n",
      "Epoch 4000 Loss: 0.24977044761180878\n",
      "Epoch 5000 Loss: 0.2496638000011444\n",
      "Epoch 6000 Loss: 0.24946792423725128\n",
      "Epoch 7000 Loss: 0.24906250834465027\n",
      "Epoch 8000 Loss: 0.24809706211090088\n",
      "Epoch 9000 Loss: 0.2454599142074585\n",
      "Predicted values:\n",
      "tensor([[0.4843],\n",
      "        [0.5800],\n",
      "        [0.4278],\n",
      "        [0.4623]])\n"
     ]
    }
   ],
   "source": [
    "# Import necessary libraries\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "\n",
    "# Define the XOR network using a sequential container\n",
    "model = nn.Sequential(\n",
    "    nn.Linear(2, 2),\n",
    "    nn.Sigmoid(),\n",
    "    nn.Linear(2, 1),\n",
    "    nn.Sigmoid()\n",
    ")\n",
    "\n",
    "# Initialize the loss function and optimizer\n",
    "criterion = nn.MSELoss()\n",
    "optimizer = optim.SGD(model.parameters(), lr=0.1)\n",
    "\n",
    "# Training data for XOR\n",
    "data = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])\n",
    "labels = torch.tensor([[0.0], [1.0], [1.0], [0.0]])\n",
    "\n",
    "# Train the model\n",
    "for epoch in range(10000):\n",
    "    # Forward pass: Compute predicted y by passing x to the model\n",
    "    pred = model(data)\n",
    "\n",
    "    # Compute and print loss\n",
    "    loss = criterion(pred, labels)\n",
    "    if epoch % 1000 == 0:\n",
    "        print(f'Epoch {epoch} Loss: {loss.item()}')\n",
    "\n",
    "    # Zero gradients, perform a backward pass, and update the weights.\n",
    "    optimizer.zero_grad()\n",
    "    loss.backward()\n",
    "    optimizer.step()\n",
    "\n",
    "# Test the model\n",
    "with torch.no_grad():\n",
    "    test_pred = model(data)\n",
    "    print(\"Predicted values:\")\n",
    "    print(test_pred)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "jY-NmeRAoPVr"
   },
   "source": [
    "If you receive an error or the output is not exactly what you like, it is effective to provide that output and any errors to the LLM. Here, we provide the output and ask the LLM if that seems correct. Sometimes, the LLM may insist that the output is correct, so you must \"debate\" the LLM, providing additional details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 1000
    },
    "id": "bzkCRwx_RxxI",
    "outputId": "e9c3628f-97e4-4c56-9082-ec38719f5f04"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model response:\n"
     ]
    },
    {
     "data": {
      "text/markdown": [
       "It appears that the model hasn't learned the XOR function effectively, as the output values are not close to the expected binary outputs of 0 or 1. This could be due to several factors including insufficient training, the need for a different network architecture, or the need for tuning hyperparameters like the learning rate or the number of epochs.\n",
       "\n",
       "Here are a few suggestions to improve the model's performance:\n",
       "\n",
       "1. **Increase the number of epochs**: Sometimes, the network needs more training time to converge to the correct values.\n",
       "2. **Adjust the learning rate**: If the learning rate is too high, the model might overshoot the minimum; if it's too low, it might not converge in a reasonable amount of time.\n",
       "3. **Add more neurons in the hidden layer**: Increasing the capacity of the network can sometimes help in capturing the complexity of the function.\n",
       "4. **Use a different optimizer**: Sometimes, changing the optimizer can lead to better results. For example, Adam is often more effective than SGD for many tasks.\n",
       "\n",
       "Let's adjust the model by increasing the number of hidden neurons and using the Adam optimizer. Here's the revised code:\n",
       "\n",
       "```python\n",
       "# Import necessary libraries\n",
       "import torch\n",
       "import torch.nn as nn\n",
       "import torch.optim as optim\n",
       "\n",
       "# Define the XOR network using a sequential container\n",
       "model = nn.Sequential(\n",
       "    nn.Linear(2, 4),  # Increased the number of neurons in the hidden layer\n",
       "    nn.Sigmoid(),\n",
       "    nn.Linear(4, 1),\n",
       "    nn.Sigmoid()\n",
       ")\n",
       "\n",
       "# Initialize the loss function and optimizer\n",
       "criterion = nn.MSELoss()\n",
       "optimizer = optim.Adam(model.parameters(), lr=0.1)  # Changed to Adam optimizer\n",
       "\n",
       "# Training data for XOR\n",
       "data = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])\n",
       "labels = torch.tensor([[0.0], [1.0], [1.0], [0.0]])\n",
       "\n",
       "# Train the model\n",
       "for epoch in range(20000):  # Increased the number of epochs\n",
       "    # Forward pass: Compute predicted y by passing x to the model\n",
       "    pred = model(data)\n",
       "\n",
       "    # Compute and print loss\n",
       "    loss = criterion(pred, labels)\n",
       "    if epoch % 1000 == 0:\n",
       "        print(f'Epoch {epoch} Loss: {loss.item()}')\n",
       "\n",
       "    # Zero gradients, perform a backward pass, and update the weights.\n",
       "    optimizer.zero_grad()\n",
       "    loss.backward()\n",
       "    optimizer.step()\n",
       "\n",
       "# Test the model\n",
       "with torch.no_grad():\n",
       "    test_pred = model(data)\n",
       "    print(\"Predicted values:\")\n",
       "    print(test_pred)\n",
       "```\n",
       "\n",
       "### Notes on Execution:\n",
       "- The number of hidden neurons has been increased to 4.\n",
       "- The optimizer has been changed from SGD to Adam.\n",
       "- The number of training epochs has been doubled to 20,000.\n",
       "- These changes are aimed at improving the model's ability to learn the XOR function more effectively. Adjust further if needed based on the output."
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "generate_code(conversation, \"\"\"The output was:\n",
    "\n",
    "Predicted values:\n",
    "tensor([[0.4843],\n",
    "        [0.5800],\n",
    "        [0.4278],\n",
    "        [0.4623]])\n",
    "\n",
    "Are you sure that is correct?\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "-r2Qpe11cWwm"
   },
   "source": [
    "## Test the Improved Version\n",
    "\n",
    "We now receive much more accurate output when we test the neural network provided."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "V19QyaROSUP1",
    "outputId": "8e8993aa-6d11-4788-b6e5-3d291c8097de"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 0 Loss: 0.26264309883117676\n",
      "Epoch 1000 Loss: 7.067574188113213e-05\n",
      "Epoch 2000 Loss: 9.918729119817726e-06\n",
      "Epoch 3000 Loss: 3.147976713080425e-06\n",
      "Epoch 4000 Loss: 1.4494128208752954e-06\n",
      "Epoch 5000 Loss: 7.723459702901891e-07\n",
      "Epoch 6000 Loss: 4.419764536578441e-07\n",
      "Epoch 7000 Loss: 2.627704134283704e-07\n",
      "Epoch 8000 Loss: 1.595393825937208e-07\n",
      "Epoch 9000 Loss: 9.79592442718058e-08\n",
      "Epoch 10000 Loss: 6.045179645752796e-08\n",
      "Epoch 11000 Loss: 3.737751441690307e-08\n",
      "Epoch 12000 Loss: 2.3105236834908283e-08\n",
      "Epoch 13000 Loss: 1.4265761016929446e-08\n",
      "Epoch 14000 Loss: 8.795730366273347e-09\n",
      "Epoch 15000 Loss: 5.41784128671452e-09\n",
      "Epoch 16000 Loss: 3.3351594819208685e-09\n",
      "Epoch 17000 Loss: 2.053748282548895e-09\n",
      "Epoch 18000 Loss: 1.2684615580127456e-09\n",
      "Epoch 19000 Loss: 7.867687057405703e-10\n",
      "Predicted values:\n",
      "tensor([[2.3532e-05],\n",
      "        [9.9998e-01],\n",
      "        [9.9998e-01],\n",
      "        [2.2704e-05]])\n"
     ]
    }
   ],
   "source": [
    "# Import necessary libraries\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "\n",
    "# Define the XOR network using a sequential container\n",
    "model = nn.Sequential(\n",
    "    nn.Linear(2, 4),  # Increased the number of neurons in the hidden layer\n",
    "    nn.Sigmoid(),\n",
    "    nn.Linear(4, 1),\n",
    "    nn.Sigmoid()\n",
    ")\n",
    "\n",
    "# Initialize the loss function and optimizer\n",
    "criterion = nn.MSELoss()\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.1)  # Changed to Adam optimizer\n",
    "\n",
    "# Training data for XOR\n",
    "data = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])\n",
    "labels = torch.tensor([[0.0], [1.0], [1.0], [0.0]])\n",
    "\n",
    "# Train the model\n",
    "for epoch in range(20000):  # Increased the number of epochs\n",
    "    # Forward pass: Compute predicted y by passing x to the model\n",
    "    pred = model(data)\n",
    "\n",
    "    # Compute and print loss\n",
    "    loss = criterion(pred, labels)\n",
    "    if epoch % 1000 == 0:\n",
    "        print(f'Epoch {epoch} Loss: {loss.item()}')\n",
    "\n",
    "    # Zero gradients, perform a backward pass, and update the weights.\n",
    "    optimizer.zero_grad()\n",
    "    loss.backward()\n",
    "    optimizer.step()\n",
    "\n",
    "# Test the model\n",
    "with torch.no_grad():\n",
    "    test_pred = model(data)\n",
    "    print(\"Predicted values:\")\n",
    "    print(test_pred)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "wCAmOVHEpAjf"
   },
   "source": [
    "## Combining the Conversation into a Single Prompt\n",
    "\n",
    "We should combine this entire conversation into a single prompt, especially if we wish to save the prompt along with the code. We can request the LLM to create this combined prompt for us."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 122
    },
    "id": "MKMcmXTNSiFw",
    "outputId": "4f81e0cd-57b3-4581-e0e9-18d129663384"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model response:\n"
     ]
    },
    {
     "data": {
      "text/markdown": [
       "Certainly! A single prompt that could lead directly to the last provided code output, which includes the modifications for improving the XOR function learning, might be:\n",
       "\n",
       "\"Can you provide a Python code using PyTorch to effectively learn the XOR function with increased hidden neurons, using the Adam optimizer, and with more training epochs?\""
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "generate_code(conversation, \"\"\"Okay, that is great, can you suggest a single\n",
    "prompt that would have resulted in this last code output?\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Mw-QDu7Rpo_T"
   },
   "source": [
    "The LLM's attempt at a consoldated prompt is incomplete. It skips several important details and does not provide precise requirements. I will manually make some improvements, which you can see here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 1000
    },
    "id": "1TzyiT6aS7ej",
    "outputId": "13c3d633-d5c7-4c57-8787-581ed09b2618"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model response:\n"
     ]
    },
    {
     "data": {
      "text/markdown": [
       "Certainly! Below is the Python code using PyTorch to learn the XOR function. This example uses a simple neural network with one hidden layer consisting of 4 neurons. The network is trained using the Adam optimizer over 20,000 epochs. The network architecture and training process are defined using a sequential model rather than subclassing `nn.Module`.\n",
       "\n",
       "### Notes on the Code\n",
       "- The XOR function is a simple binary classification problem where the output is true if the inputs are not equal.\n",
       "- The network uses a single hidden layer with 4 neurons, which should be sufficient to learn the XOR pattern.\n",
       "- ReLU activation is used for non-linearity in the hidden layer.\n",
       "- The output layer uses a sigmoid activation function to output a probability between 0 and 1.\n",
       "- Binary cross-entropy loss is used as it is suitable for binary classification tasks.\n",
       "- The Adam optimizer is known for being effective in various conditions and is used here for optimization.\n",
       "- The input and target tensors are explicitly defined to represent the XOR truth table.\n",
       "\n",
       "```python\n",
       "import torch\n",
       "import torch.nn as nn\n",
       "import torch.optim as optim\n",
       "\n",
       "# Define the XOR inputs and outputs\n",
       "inputs = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=torch.float)\n",
       "targets = torch.tensor([[0], [1], [1], [0]], dtype=torch.float)\n",
       "\n",
       "# Define the model using a sequential container\n",
       "model = nn.Sequential(\n",
       "    nn.Linear(2, 4),  # Input layer to hidden layer with 4 neurons\n",
       "    nn.ReLU(),        # ReLU activation function\n",
       "    nn.Linear(4, 1),  # Hidden layer to output layer\n",
       "    nn.Sigmoid()      # Sigmoid activation function for binary output\n",
       ")\n",
       "\n",
       "# Define the loss function and the optimizer\n",
       "criterion = nn.BCELoss()  # Binary Cross-Entropy Loss\n",
       "optimizer = optim.Adam(model.parameters(), lr=0.01)  # Adam optimizer with learning rate of 0.01\n",
       "\n",
       "# Training loop\n",
       "for epoch in range(20000):  # 20,000 training epochs\n",
       "    optimizer.zero_grad()   # Clear gradients for each training step\n",
       "    outputs = model(inputs)  # Forward pass: compute predicted outputs by passing inputs to the model\n",
       "    loss = criterion(outputs, targets)  # Compute loss\n",
       "    loss.backward()  # Backward pass: compute gradient of the loss with respect to model parameters\n",
       "    optimizer.step()  # Perform a single optimization step (parameter update)\n",
       "\n",
       "    if (epoch + 1) % 1000 == 0:\n",
       "        print(f'Epoch [{epoch + 1}/20000], Loss: {loss.item():.4f}')\n",
       "\n",
       "# Testing the model\n",
       "with torch.no_grad():  # Context-manager that disabled gradient calculation\n",
       "    predicted = model(inputs).round()  # Forward pass and rounding off to get predictions\n",
       "    print(f'Predicted tensor: {predicted}')\n",
       "    print(f'Actual tensor: {targets}')\n",
       "```\n",
       "\n",
       "### Notes on Execution\n",
       "- Ensure that PyTorch is installed in your Python environment to run this code.\n",
       "- The learning rate and the number of epochs are set to typical starting values but may need adjustment based on the specific behavior of the training process.\n",
       "- The print statements inside the loop provide a simple way to monitor the training progress.\n",
       "- The final output shows the predicted values and actual values for the XOR function, allowing you to verify the model's performance."
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Start a new conversation\n",
    "conversation = start_conversation()\n",
    "generate_code(conversation, \"\"\"\n",
    "Can you provide Python code using PyTorch to effectively learn the XOR function\n",
    "with 4 hidden neurons, using the Adam optimizer, and 20K training epochs?\n",
    "Use a sequence not a nn.Module class.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "0EiMd01ATosU"
   },
   "source": [
    "## Test the Final Prompt\n",
    "\n",
    "Now, we test the final prompt. My prompt produces an acceptable result, but there are some opportunities for improvement. You can specify the exact format for the output. For example, sometimes code is generated to round the results, but other times it is not."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "Fx0_Al1kTmh7",
    "outputId": "131bc687-76a4-4ec4-cb62-0ef08e815be7"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch [1000/20000], Loss: 0.0034\n",
      "Epoch [2000/20000], Loss: 0.0009\n",
      "Epoch [3000/20000], Loss: 0.0004\n",
      "Epoch [4000/20000], Loss: 0.0002\n",
      "Epoch [5000/20000], Loss: 0.0001\n",
      "Epoch [6000/20000], Loss: 0.0001\n",
      "Epoch [7000/20000], Loss: 0.0000\n",
      "Epoch [8000/20000], Loss: 0.0000\n",
      "Epoch [9000/20000], Loss: 0.0000\n",
      "Epoch [10000/20000], Loss: 0.0000\n",
      "Epoch [11000/20000], Loss: 0.0000\n",
      "Epoch [12000/20000], Loss: 0.0000\n",
      "Epoch [13000/20000], Loss: 0.0000\n",
      "Epoch [14000/20000], Loss: 0.0000\n",
      "Epoch [15000/20000], Loss: 0.0000\n",
      "Epoch [16000/20000], Loss: 0.0000\n",
      "Epoch [17000/20000], Loss: 0.0000\n",
      "Epoch [18000/20000], Loss: 0.0000\n",
      "Epoch [19000/20000], Loss: 0.0000\n",
      "Epoch [20000/20000], Loss: 0.0000\n",
      "Predicted tensor: tensor([[0.],\n",
      "        [1.],\n",
      "        [1.],\n",
      "        [0.]])\n",
      "Actual tensor: tensor([[0.],\n",
      "        [1.],\n",
      "        [1.],\n",
      "        [0.]])\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "\n",
    "# Define the XOR inputs and outputs\n",
    "inputs = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=torch.float)\n",
    "targets = torch.tensor([[0], [1], [1], [0]], dtype=torch.float)\n",
    "\n",
    "# Define the model using a sequential container\n",
    "model = nn.Sequential(\n",
    "    nn.Linear(2, 4),  # Input layer to hidden layer with 4 neurons\n",
    "    nn.ReLU(),        # ReLU activation function\n",
    "    nn.Linear(4, 1),  # Hidden layer to output layer\n",
    "    nn.Sigmoid()      # Sigmoid activation function for binary output\n",
    ")\n",
    "\n",
    "# Define the loss function and the optimizer\n",
    "criterion = nn.BCELoss()  # Binary Cross-Entropy Loss\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.01)  # Adam optimizer with learning rate of 0.01\n",
    "\n",
    "# Training loop\n",
    "for epoch in range(20000):  # 20,000 training epochs\n",
    "    optimizer.zero_grad()   # Clear gradients for each training step\n",
    "    outputs = model(inputs)  # Forward pass: compute predicted outputs by passing inputs to the model\n",
    "    loss = criterion(outputs, targets)  # Compute loss\n",
    "    loss.backward()  # Backward pass: compute gradient of the loss with respect to model parameters\n",
    "    optimizer.step()  # Perform a single optimization step (parameter update)\n",
    "\n",
    "    if (epoch + 1) % 1000 == 0:\n",
    "        print(f'Epoch [{epoch + 1}/20000], Loss: {loss.item():.4f}')\n",
    "\n",
    "# Testing the model\n",
    "with torch.no_grad():  # Context-manager that disabled gradient calculation\n",
    "    predicted = model(inputs).round()  # Forward pass and rounding off to get predictions\n",
    "    print(f'Predicted tensor: {predicted}')\n",
    "    print(f'Actual tensor: {targets}')"
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3.11 (genai)",
   "language": "python",
   "name": "genai"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.8"
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
